// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include <list>
#include <memory>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/macros.h"
#include "base/rand_util.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
#include "media/audio/audio_io.h"
#include "media/audio/simple_sources.h"
#include "media/audio/virtual_audio_input_stream.h"
#include "media/audio/virtual_audio_output_stream.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::AtLeast;
using ::testing::InvokeWithoutArgs;
using ::testing::NotNull;

namespace media {

namespace {

    const AudioParameters kParams(
        AudioParameters::AUDIO_PCM_LOW_LATENCY, CHANNEL_LAYOUT_STEREO, 8000, 8, 10);

    class MockInputCallback : public AudioInputStream::AudioInputCallback {
    public:
        MockInputCallback()
            : data_pushed_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
                base::WaitableEvent::InitialState::NOT_SIGNALED)
        {
            ON_CALL(*this, OnData(_, _, _, _)).WillByDefault(InvokeWithoutArgs(&data_pushed_, &base::WaitableEvent::Signal));
        }

        virtual ~MockInputCallback() { }

        MOCK_METHOD4(OnData,
            void(AudioInputStream* stream,
                const AudioBus* source,
                uint32_t hardware_delay_bytes,
                double volume));
        MOCK_METHOD1(OnError, void(AudioInputStream* stream));

        void WaitForDataPushes()
        {
            for (int i = 0; i < 3; ++i) {
                data_pushed_.Wait();
            }
        }

    private:
        base::WaitableEvent data_pushed_;

        DISALLOW_COPY_AND_ASSIGN(MockInputCallback);
    };

    class TestAudioSource : public SineWaveAudioSource {
    public:
        TestAudioSource()
            : SineWaveAudioSource(kParams.channel_layout(),
                200.0,
                kParams.sample_rate())
            , data_pulled_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
                  base::WaitableEvent::InitialState::NOT_SIGNALED)
        {
        }

        ~TestAudioSource() override { }

        int OnMoreData(base::TimeDelta delay,
            base::TimeTicks delay_timestamp,
            int prior_frames_skipped,
            AudioBus* dest) override
        {
            const int ret = SineWaveAudioSource::OnMoreData(delay, delay_timestamp,
                prior_frames_skipped, dest);
            data_pulled_.Signal();
            return ret;
        }

        void WaitForDataPulls()
        {
            for (int i = 0; i < 3; ++i) {
                data_pulled_.Wait();
            }
        }

    private:
        base::WaitableEvent data_pulled_;

        DISALLOW_COPY_AND_ASSIGN(TestAudioSource);
    };

} // namespace

class VirtualAudioInputStreamTest : public testing::TestWithParam<bool> {
public:
    VirtualAudioInputStreamTest()
        : audio_thread_(new base::Thread("AudioThread"))
        , worker_thread_(new base::Thread("AudioWorkerThread"))
        , stream_(NULL)
        , closed_stream_(base::WaitableEvent::ResetPolicy::AUTOMATIC,
              base::WaitableEvent::InitialState::NOT_SIGNALED)
    {
        audio_thread_->Start();
        audio_task_runner_ = audio_thread_->task_runner();
    }

    virtual ~VirtualAudioInputStreamTest()
    {
        SyncWithAudioThread();

        DCHECK(output_streams_.empty());
        DCHECK(stopped_output_streams_.empty());
    }

    void Create()
    {
        const bool worker_is_separate_thread = GetParam();
        stream_ = new VirtualAudioInputStream(
            kParams, GetWorkerTaskRunner(worker_is_separate_thread),
            base::Bind(&base::DeletePointer<VirtualAudioInputStream>));
        stream_->Open();
    }

    void Start()
    {
        EXPECT_CALL(input_callback_, OnData(_, NotNull(), _, _)).Times(AtLeast(1));

        ASSERT_TRUE(stream_);
        stream_->Start(&input_callback_);
    }

    void CreateAndStartOneOutputStream()
    {
        ASSERT_TRUE(stream_);
        AudioOutputStream* const output_stream = new VirtualAudioOutputStream(
            kParams,
            stream_,
            base::Bind(&base::DeletePointer<VirtualAudioOutputStream>));
        output_streams_.push_back(output_stream);

        output_stream->Open();
        output_stream->Start(&source_);
    }

    void Stop()
    {
        ASSERT_TRUE(stream_);
        stream_->Stop();
    }

    void Close()
    {
        ASSERT_TRUE(stream_);
        stream_->Close();
        stream_ = NULL;
        closed_stream_.Signal();
    }

    void WaitForDataToFlow()
    {
        // Wait until audio thread is idle before calling output_streams_.size().
        SyncWithAudioThread();

        const int count = output_streams_.size();
        for (int i = 0; i < count; ++i) {
            source_.WaitForDataPulls();
        }

        input_callback_.WaitForDataPushes();
    }

    void WaitUntilClosed()
    {
        closed_stream_.Wait();
    }

    void StopAndCloseOneOutputStream()
    {
        ASSERT_TRUE(!output_streams_.empty());
        AudioOutputStream* const output_stream = output_streams_.front();
        ASSERT_TRUE(output_stream);
        output_streams_.pop_front();

        output_stream->Stop();
        output_stream->Close();
    }

    void StopFirstOutputStream()
    {
        ASSERT_TRUE(!output_streams_.empty());
        AudioOutputStream* const output_stream = output_streams_.front();
        ASSERT_TRUE(output_stream);
        output_streams_.pop_front();
        output_stream->Stop();
        stopped_output_streams_.push_back(output_stream);
    }

    void StopSomeOutputStreams()
    {
        ASSERT_LE(2, static_cast<int>(output_streams_.size()));
        for (int remaning = base::RandInt(1, output_streams_.size() - 1);
             remaning > 0; --remaning) {
            StopFirstOutputStream();
        }
    }

    void RestartAllStoppedOutputStreams()
    {
        typedef std::list<AudioOutputStream*>::const_iterator ConstIter;
        for (ConstIter it = stopped_output_streams_.begin();
             it != stopped_output_streams_.end(); ++it) {
            (*it)->Start(&source_);
            output_streams_.push_back(*it);
        }
        stopped_output_streams_.clear();
    }

    const scoped_refptr<base::SingleThreadTaskRunner>& audio_task_runner() const
    {
        return audio_task_runner_;
    }

    const scoped_refptr<base::SingleThreadTaskRunner>& GetWorkerTaskRunner(
        bool worker_is_separate_thread)
    {
        if (worker_is_separate_thread) {
            if (!worker_thread_->IsRunning()) {
                worker_thread_->Start();
                worker_task_runner_ = worker_thread_->task_runner();
            }
            return worker_task_runner_;
        } else {
            return audio_task_runner_;
        }
    }

private:
    void SyncWithAudioThread()
    {
        base::WaitableEvent done(base::WaitableEvent::ResetPolicy::AUTOMATIC,
            base::WaitableEvent::InitialState::NOT_SIGNALED);
        audio_task_runner_->PostTask(
            FROM_HERE,
            base::Bind(&base::WaitableEvent::Signal, base::Unretained(&done)));
        done.Wait();
    }

    std::unique_ptr<base::Thread> audio_thread_;
    scoped_refptr<base::SingleThreadTaskRunner> audio_task_runner_;
    std::unique_ptr<base::Thread> worker_thread_;
    scoped_refptr<base::SingleThreadTaskRunner> worker_task_runner_;

    VirtualAudioInputStream* stream_;
    MockInputCallback input_callback_;
    base::WaitableEvent closed_stream_;

    std::list<AudioOutputStream*> output_streams_;
    std::list<AudioOutputStream*> stopped_output_streams_;
    TestAudioSource source_;

    DISALLOW_COPY_AND_ASSIGN(VirtualAudioInputStreamTest);
};

#define RUN_ON_AUDIO_THREAD(method) \
    audio_task_runner()->PostTask(  \
        FROM_HERE, base::Bind(&VirtualAudioInputStreamTest::method, base::Unretained(this)))

TEST_P(VirtualAudioInputStreamTest, CreateAndClose)
{
    RUN_ON_AUDIO_THREAD(Create);
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

TEST_P(VirtualAudioInputStreamTest, NoOutputs)
{
    RUN_ON_AUDIO_THREAD(Create);
    RUN_ON_AUDIO_THREAD(Start);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(Stop);
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

TEST_P(VirtualAudioInputStreamTest, SingleOutput)
{
    RUN_ON_AUDIO_THREAD(Create);
    RUN_ON_AUDIO_THREAD(Start);
    RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    RUN_ON_AUDIO_THREAD(Stop);
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

TEST_P(VirtualAudioInputStreamTest, SingleOutputPausedAndRestarted)
{
    RUN_ON_AUDIO_THREAD(Create);
    RUN_ON_AUDIO_THREAD(Start);
    RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopFirstOutputStream);
    RUN_ON_AUDIO_THREAD(RestartAllStoppedOutputStreams);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    RUN_ON_AUDIO_THREAD(Stop);
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

TEST_P(VirtualAudioInputStreamTest, MultipleOutputs)
{
    RUN_ON_AUDIO_THREAD(Create);
    RUN_ON_AUDIO_THREAD(Start);
    RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopFirstOutputStream);
    RUN_ON_AUDIO_THREAD(StopFirstOutputStream);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopFirstOutputStream);
    RUN_ON_AUDIO_THREAD(RestartAllStoppedOutputStreams);
    WaitForDataToFlow();
    RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    RUN_ON_AUDIO_THREAD(Stop);
    RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

// A combination of all of the above tests with many output streams.
TEST_P(VirtualAudioInputStreamTest, ComprehensiveTest)
{
    static const int kNumOutputs = 8;
    static const int kHalfNumOutputs = kNumOutputs / 2;
    static const int kPauseIterations = 5;

    RUN_ON_AUDIO_THREAD(Create);
    for (int i = 0; i < kHalfNumOutputs; ++i) {
        RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    }
    RUN_ON_AUDIO_THREAD(Start);
    WaitForDataToFlow();
    for (int i = 0; i < kHalfNumOutputs; ++i) {
        RUN_ON_AUDIO_THREAD(CreateAndStartOneOutputStream);
    }
    WaitForDataToFlow();
    for (int i = 0; i < kPauseIterations; ++i) {
        RUN_ON_AUDIO_THREAD(StopSomeOutputStreams);
        WaitForDataToFlow();
        RUN_ON_AUDIO_THREAD(RestartAllStoppedOutputStreams);
        WaitForDataToFlow();
    }
    for (int i = 0; i < kHalfNumOutputs; ++i) {
        RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    }
    RUN_ON_AUDIO_THREAD(Stop);
    for (int i = 0; i < kHalfNumOutputs; ++i) {
        RUN_ON_AUDIO_THREAD(StopAndCloseOneOutputStream);
    }
    RUN_ON_AUDIO_THREAD(Close);
    WaitUntilClosed();
}

INSTANTIATE_TEST_CASE_P(SingleVersusMultithreaded,
    VirtualAudioInputStreamTest,
    ::testing::Values(false, true));

} // namespace media
